/**
 * @Copyright FangYun Home
 */
package home.fangyun.pagerank;

import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import home.fangyun.pagerank.domain.Assessable;
import home.fangyun.pagerank.domain.Rank;
import home.fangyun.pagerank.domain.importance.Thing;
import home.fangyun.pagerank.domain.importance.ThingAssessment;

/**
 * @author fangyuncn@163.com
 */
public class Ranking {
	private Assessable<Thing> assessment;
	/**
	 * The calculation will stop if the difference of PageRank values between
	 * iterations change less than this value
	 */
	private final double tolerance;

	/**
	 * Maximum iterations to run
	 */
	private final int maxIterations;

	/**
	 * Damping factor
	 */
	private final BigDecimal dampingFactor;

	public Ranking(Path path) throws IOException {
		assessment = new ThingAssessment(path);
		tolerance = 0.0001;
		maxIterations = 100;
		dampingFactor = new BigDecimal(0.15);
	}

	public BigDecimal[] computeWeights() {
		if (assessment == null) {
			return null;
		}
		BigDecimal[][] A = votes2stochastic(assessment);
		BigDecimal[][] M = googleMatrix(A);
		BigDecimal[] v = vector(assessment);
		BigDecimal[] previousRank = v;
		BigDecimal[] rank = v;
		for (int i = 0; i < maxIterations; i++) {
			rank = multiple(M, previousRank);
			printWeights(i, rank);
			boolean accepted = true;
			for (int k = 0; k < rank.length; k++) {
				if (previousRank[k].subtract(rank[k]).doubleValue() > tolerance) {
					accepted = false;
				}
			}
			if (accepted) {
				return rank;
			}
			previousRank = rank;
		}
		return rank; // Maximum iterations exceeded
	}

	/**
	 * Google Matrix
	 * 
	 * @param A
	 * @return
	 */
	private BigDecimal[][] googleMatrix(BigDecimal[][] A) {
		if (assessment == null) {
			return null;

		}
		List<Thing> things = assessment.getVotes();
		int n = things.size();
		BigDecimal oneMinusP = BigDecimal.ONE.subtract(dampingFactor);
		BigDecimal[][] B = B(n);
		BigDecimal[][] M = zeros(n);
		for (int i = 0; i < A.length; i++) {
			for (int j = 0; j < A[i].length; j++) {
				BigDecimal m = A[i][j].multiply(oneMinusP).add(B[i][j].multiply(dampingFactor));
				M[i][j] = m;
			}
		}
		return M;
	}

	private BigDecimal[] multiple(BigDecimal[][] matrix, BigDecimal[] vector) {
		BigDecimal[] r = new BigDecimal[vector.length];
		for (int i = 0; i < r.length; i++) {
			r[i] = BigDecimal.ZERO;
		}
		for (int i = 0; i < matrix.length; i++) {
			for (int j = 0; j < matrix[i].length; j++) {
				BigDecimal p = matrix[i][j];
				r[i] = r[i].add(vector[j].multiply(p));
			}
		}
		return r;
	}

	/**
	 * Generate adjacency matrix from votes of assessment.
	 * 
	 * @param assessment
	 * @return
	 */
	private BigDecimal[][] votes2matrix(Assessable<Thing> assessment) {
		if (assessment == null) {
			return null;
		}
		List<Thing> votes = assessment.getVotes();
		assert votes.size() == assessment.getVotes().size();
		int n = votes.size();
		BigDecimal[][] m = zeros(n);
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < votes.get(i).getToRatings().length; j++) {
				int[][] r = votes.get(i).getToRatings();
				for (int l = 0; l < r.length; l++) {
					int to = r[l][0];
					for (int k = 0; k < n; k++) {
						if (to == k + 1) {
							m[i][k] = BigDecimal.ONE;
							break;
						}
					}
				}				
			}
		}
		return m;
	}

	/**
	 * Generate column stochastic matrix from votes of assessment.
	 * 
	 * @param assessment
	 * @return
	 */
	private BigDecimal[][] votes2stochastic(Assessable<Thing> assessment) {
		if (assessment == null) {
			return null;

		}
		List<Thing> votes = assessment.getVotes();
		int n = votes.size();
		BigDecimal[][] stochastic = zeros(n);
		BigDecimal[][] m = votes2matrix(assessment);
		for (int j = 0; j < n; j++) { // j column
			int toN = sumOfToRatings(votes, j);
			if (toN > 0) {
				for (int i = 0; i < n; i++) {
					if (m[i][j].compareTo(BigDecimal.ZERO) > 0) {
						int[][] r = votes.get(j).getToRatings();
						for (int l = 0; l < r.length; l++) {
							int to = r[l][0];
							for (int k = 0; k < n; k++) {
								if (to == k + 1) {
									stochastic[k][j] = new BigDecimal((1.0d * r[l][1]) / toN);
									break;
								}
							}
						}	
					}
				}
			}
		}
		return stochastic;
	}

	/**
	 * Initial weight vector.
	 * 
	 * @param assessment
	 * @return
	 */
	private BigDecimal[] vector(Assessable<Thing> assessment) {
		if (assessment == null) {
			return null;

		}
		int idLength = assessment.getVotes().size();
		BigDecimal[] v = new BigDecimal[idLength];
		for (int i = 0; i < v.length; i++) {
			v[i] = new BigDecimal(1.0d / idLength);
		}
		return v;
	}

	private int sumOfToRatings(List<Thing> votes, int from) {
		if (votes == null) {
			return 0;
		}
		int[][] row = votes.get(from).getToRatings();
		int sum = 0;
		for (int i = 0; i < row.length; i++) {
			sum += row[i][1];
		}
		return sum;
	}

	private BigDecimal[][] zeros(int n) {
		BigDecimal[][] z = new BigDecimal[n][n];
		for (int i = 0; i < z.length; i++) {
			for (int j = 0; j < z[i].length; j++) {
				z[i][j] = BigDecimal.ZERO;
			}
		}
		return z;
	}

	private BigDecimal[][] B(int n) {
		BigDecimal[][] b = new BigDecimal[n][n];
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				b[i][j] = new BigDecimal(1.0d / n);
			}
		}
		return b;
	}

	private void printWeights(int i, BigDecimal[] weights) {
		if (weights == null) {
			return;
		}
		double s = Arrays.stream(weights).mapToDouble(BigDecimal::doubleValue).sum();
		System.out.format("Sum=%f, Iteration=%d, Weights=%s\n", s, i,
				Arrays.toString(Arrays.stream(weights).mapToDouble(BigDecimal::doubleValue).toArray()));
	}

	public void printRank(BigDecimal[] weights) {
		if (weights == null) {
			return;
		}
		List<Rank<Thing>> r = new ArrayList<>(weights.length);
		for (int i = 0; i < weights.length; i++) {
			r.add(new Rank<Thing>(i + 1, weights[i], assessment.getVotes().get(i)));
		}
		r.stream().sorted(Comparator.reverseOrder()).forEach(System.out::println);
	}

}
